/*
 * Copyright 2016 The Native Client Authors. All rights reserved.
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 */

/*
 * NaCl tests for limited file access
 */

#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>

#include "native_client/src/include/nacl_assert.h"
#include "native_client/src/trusted/service_runtime/nacl_config.h"

namespace {

/* Global pathnames (randomly generated by SCons) */
char g_temp_dir_name[PATH_MAX];
char g_temp_dir_path[PATH_MAX];
char g_temp_file_name[PATH_MAX];
char g_temp_file_path[PATH_MAX];
char g_temp_symlink_name[PATH_MAX];
char g_temp_symlink_path[PATH_MAX];
char g_temp_sub_dir_name[PATH_MAX];
char g_temp_sub_dir_path[PATH_MAX];
char g_temp_sub_file_name[PATH_MAX];
char g_temp_sub_file_path[PATH_MAX];
char g_temp_inaccessible_dir_name[PATH_MAX];
char g_temp_inaccessible_file_name[PATH_MAX];

/*
 * function passed(testname, msg)
 *   print success message
 */
void passed(const char *testname, const char *msg) {
  printf("TEST PASSED: %s: %s\n", testname, msg);
}

/*
 * Helper function which tests out basic file access.
 *  Opens a file (possibly new),
 *  Writes a test string to the file,
 *  Seeks to the beginning of the file,
 *  Reads, verifying the original write persisted, and
 *  Closes the file.
 *
 * @param[in] filename Name of the file which is opened.
 * @param[in] new_file Boolean indicating if the |filename| to be opened is new.
 */
void do_test_write_read_file(const char *filename, bool new_file) {
  printf("File: %s\n", filename);
  char buf[5];
  char test_string[6] = "abcde";
  int test_string_len = 5;
  int new_file_flags = new_file ? (O_CREAT | O_TRUNC) : 0;
  int fd = open(filename, O_RDWR | new_file_flags, S_IRUSR | S_IWUSR);
  ASSERT_MSG(fd >= 0, "open() failed\n");

  ASSERT_EQ(test_string_len, write(fd, test_string, test_string_len));
  ASSERT_EQ(0, lseek(fd, 0, SEEK_SET));
  ASSERT_EQ(test_string_len, read(fd, buf, test_string_len));
  ASSERT_EQ(0, memcmp(buf, test_string, test_string_len));
  ASSERT_EQ(0, close(fd));
}

void test_directory_walk() {
  // Attempt to walk down valid directory structure (and back again).
  char dirname[PATH_MAX];
  ASSERT_NE_MSG(getcwd(dirname, PATH_MAX), NULL, "getcwd() failed");
  ASSERT_EQ(strcmp(dirname, "/"), 0);

  ASSERT_EQ_MSG(chdir("."), 0, "chdir() failed");
  ASSERT_EQ_MSG(chdir("/"), 0, "chdir() failed");

  DIR *d = opendir(dirname);
  ASSERT_NE_MSG(d, NULL, "opendir() failed");
  int count = 0;
  struct dirent *ent;

  /*
   * We expect to see:
   * temp_file
   * temp_symlink
   * sub_temp_dir
   * ..
   * .
   */

  bool temp_file_seen = false;
  bool temp_symlink_seen = false;
  bool sub_temp_dir_seen = false;
  bool parent_directory_seen = false;
  bool current_directory_seen = false;
  while (1) {
    ent = readdir(d);
    if (!ent)
      break;
    count++;
    if (!strncmp(ent->d_name, g_temp_file_name, strlen(g_temp_file_name))) {
      temp_file_seen = true;
    } else if (!strncmp(ent->d_name, g_temp_symlink_name,
                        strlen(g_temp_symlink_name))) {
      temp_symlink_seen = true;
    } else if (!strncmp(ent->d_name, g_temp_sub_dir_name,
                        strlen(g_temp_sub_dir_name))) {
      sub_temp_dir_seen = true;
    } else if (!strncmp(ent->d_name, "..", 2)) {
      parent_directory_seen = true;
    } else if (!strncmp(ent->d_name, ".", 1)) {
      current_directory_seen = true;
    }
  }
  ASSERT_EQ_MSG(closedir(d), 0, "closedir() failed");

  ASSERT(temp_file_seen);
  ASSERT(temp_symlink_seen);
  ASSERT(sub_temp_dir_seen);
  ASSERT(parent_directory_seen);
  ASSERT(current_directory_seen);
  ASSERT_EQ(count, 5);

  // Chdir with relative path name
  ASSERT_EQ_MSG(chdir(g_temp_sub_dir_name), 0, "chdir() failed");
  ASSERT_NE_MSG(getcwd(dirname, PATH_MAX), NULL, "getcwd() failed");
  ASSERT_EQ(strcmp(dirname, g_temp_sub_dir_path), 0);

  // Chdir with absolute path name
  ASSERT_EQ_MSG(chdir(g_temp_sub_dir_path), 0, "chdir() failed");
  ASSERT_NE_MSG(getcwd(dirname, PATH_MAX), NULL, "getcwd() failed");
  ASSERT_EQ(strcmp(dirname, g_temp_sub_dir_path), 0);

  d = opendir(dirname);
  count = 0;

  /*
   * We expect to see:
   * temp_sub_file
   * ..
   * .
   */

  bool sub_temp_file_seen = false;
  parent_directory_seen = false;
  current_directory_seen = false;
  while (1) {
    ent = readdir(d);
    if (!ent)
      break;
    count++;
    if (!strncmp(ent->d_name, g_temp_sub_file_name,
                 strlen(g_temp_sub_file_name))) {
      sub_temp_file_seen = true;
    } else if (!strncmp(ent->d_name, "..", 2)) {
      parent_directory_seen = true;
    } else if (!strncmp(ent->d_name, ".", 1)) {
      current_directory_seen = true;
    }
  }
  ASSERT_EQ_MSG(closedir(d), 0, "closedir() failed");

  ASSERT(sub_temp_file_seen);
  ASSERT(parent_directory_seen);
  ASSERT(current_directory_seen);
  ASSERT_EQ(count, 3);

  ASSERT_EQ_MSG(chdir("/"), 0, "chdir() failed");
  ASSERT_NE_MSG(getcwd(dirname, PATH_MAX), NULL, "getcwd() failed");
  ASSERT_EQ(strcmp(dirname, "/"), 0);

  passed("test_directory_walk", "all");
}

void test_new_directory_access() {
  // Create a new directory, removes that directory.
  mode_t mode = S_IRUSR | S_IWUSR | S_IXUSR;
  ASSERT_EQ(mkdir("/test_dir", mode), 0);
  ASSERT_EQ(rmdir("/test_dir"), 0);

  ASSERT_EQ(mkdir("/test_dir/", mode), 0);
  ASSERT_EQ(rmdir("/test_dir/"), 0);

  // Test that relative paths can also be used.
  ASSERT_EQ(mkdir("test_dir", mode), 0);
  ASSERT_EQ(rmdir("test_dir"), 0);

  // Test that directory contents cannot be accessed by relative path if the cwd
  // is no longer valid.
  ASSERT_EQ(mkdir("sub_dir", mode), 0);
  ASSERT_EQ(chdir("sub_dir"), 0);
  ASSERT_EQ(rmdir("../sub_dir"), 0);
  ASSERT_EQ(open("xxx", O_RDWR | O_CREAT, S_IRUSR | S_IWUSR), -1);
  ASSERT_EQ(errno, ENOENT);
  ASSERT_EQ(chdir("/"), 0);

  char file_name[PATH_MAX];
  snprintf(file_name, PATH_MAX, "%s/test_dir", g_temp_sub_dir_path);
  ASSERT_EQ(mkdir(file_name, mode), 0);
  ASSERT_EQ(rmdir(file_name), 0);

  ASSERT_NE(mkdir("/this_dir_does_not_exist/sub_dir", mode), 0);
  passed("test_new_directory_access", "all");
}

void test_link_access() {
  // Tests that we can create hard links within mounted directory.

  // Tests we can make hard link to a temporary file in the root directory.
  ASSERT_EQ(link(g_temp_file_path, "/temp_file_hard_link"), 0);
  ASSERT_EQ(unlink("/temp_file_hard_link"), 0);

  char link_name[PATH_MAX];
  // Tests we can make hard link to a temporary file in a subdirectory.
  snprintf(link_name, PATH_MAX, "%s/temp_file_hard_link", g_temp_sub_dir_path);
  ASSERT_EQ(link(g_temp_file_path, link_name), 0);
  ASSERT_EQ(unlink(link_name), 0);
  passed("test_link_access", "all");
}

void test_symlink_access() {
  // Tests that symlink and readlink access are disabled.
  char symlink_name[] = "temp_file_symlink";
  char link_dest[PATH_MAX];

  // Symlink is disabled
  ASSERT_EQ(symlink(g_temp_file_path, symlink_name), -1);
  ASSERT_EQ(errno, EACCES);
  // Readlink is disabled
  ASSERT_EQ(readlink(g_temp_symlink_path, link_dest, sizeof link_dest), -1);
  ASSERT_EQ(errno, EACCES);
  // Cannot open symlinks (even if they already exist)
  ASSERT_EQ(open(g_temp_symlink_path, O_RDONLY), -1);
  ASSERT_EQ(errno, EACCES);

  passed("test_symlink_access", "all");
}

void test_rename_access() {
  // Demonstrates that files can be renamed within the mounted directory.
  char new_file_location[PATH_MAX];
  snprintf(new_file_location, PATH_MAX, "/%s/temp_file_new",
           g_temp_sub_dir_name);
  ASSERT_EQ(rename(g_temp_file_path, new_file_location), 0);
  ASSERT_EQ(rename(new_file_location, g_temp_file_path), 0);
  // Check that the file is in our final location.
  do_test_write_read_file(g_temp_file_path, false);

  // Demonstrates that symlinks cannot be renamed within the mounted directory.
  ASSERT_EQ(rename(g_temp_symlink_path, new_file_location), -1);
  ASSERT_EQ(errno, EACCES);

  passed("test_rename_access", "all");
}

void test_escape_attempt() {
  // Try to escape the directory -- should not be able to do so.

  // Attempting to leave the root directory via ".." places the cwd at the root
  // of the mounted directory.
  ASSERT_EQ(chdir("/.."), 0);
  char cwd[PATH_MAX];
  ASSERT_EQ(getcwd(cwd, PATH_MAX), cwd);
  ASSERT_EQ(strcmp(cwd, "/"), 0);

  char inaccessible_path[PATH_MAX];
  // Cannot open files outside root.
  snprintf(inaccessible_path, PATH_MAX, "../%s", g_temp_inaccessible_file_name);
  ASSERT_EQ(open(inaccessible_path, O_RDWR, S_IRUSR | S_IWUSR), -1);
  ASSERT_EQ(errno, ENOENT);

  snprintf(inaccessible_path, PATH_MAX, "/../%s",
           g_temp_inaccessible_file_name);
  ASSERT_EQ(open(inaccessible_path, O_RDWR, S_IRUSR | S_IWUSR), -1);
  ASSERT_EQ(errno, ENOENT);

  // Cannot open directories outside root.
  struct stat buf;
  ASSERT_EQ(stat(".", &buf), 0);
  const ino_t root_inode = buf.st_ino;
  ASSERT_EQ(stat("/", &buf), 0);
  ASSERT_EQ(root_inode, buf.st_ino);
  ASSERT_EQ(stat("..", &buf), 0);
  ASSERT_EQ(root_inode, buf.st_ino);
  ASSERT_EQ(stat("/..", &buf), 0);
  ASSERT_EQ(root_inode, buf.st_ino);
  ASSERT_EQ(stat("//..", &buf), 0);
  ASSERT_EQ(root_inode, buf.st_ino);
  ASSERT_EQ(stat("/../", &buf), 0);
  ASSERT_EQ(root_inode, buf.st_ino);
  ASSERT_EQ(stat("/..//", &buf), 0);
  ASSERT_EQ(root_inode, buf.st_ino);
  ASSERT_EQ(stat("/.././//../..", &buf), 0);
  ASSERT_EQ(root_inode, buf.st_ino);

  snprintf(inaccessible_path, PATH_MAX, "../%s", g_temp_inaccessible_dir_name);
  ASSERT_EQ(opendir(inaccessible_path), NULL);
  ASSERT_EQ(errno, ENOENT);

  passed("test_escape_attempt", "all");
}

void test_information_leak() {
  // Try to determine the name of our mounted root. If possible, this
  // information leak could also lead to discovering directories and files
  // outside the mount point.
  char path[PATH_MAX];
  struct stat buf;

  ASSERT_EQ_MSG(chdir("/"), 0, "chdir() failed");

  // We should be able to access the root directory.
  ASSERT_EQ(stat("/", &buf), 0);
  const ino_t root_inode = buf.st_ino;
  ASSERT_EQ(stat("//", &buf), 0);
  ASSERT_EQ(root_inode, buf.st_ino);
  ASSERT_EQ(stat("/./.", &buf), 0);
  ASSERT_EQ(root_inode, buf.st_ino);
  ASSERT_EQ(stat("/./////.", &buf), 0);
  ASSERT_EQ(root_inode, buf.st_ino);
  ASSERT_EQ(stat(".", &buf), 0);
  ASSERT_EQ(root_inode, buf.st_ino);

  // '/..' should equal '/'.
  ASSERT_EQ(stat("/..", &buf), 0);
  ASSERT_EQ(root_inode, buf.st_ino);

  // We should not be able to identify our mount point this way.
  snprintf(path, PATH_MAX, "/../%s", g_temp_dir_name);
  ASSERT_EQ(stat(path, &buf), -1);
  ASSERT_EQ(errno, ENOENT);
  snprintf(path, PATH_MAX, "//../%s", g_temp_dir_name);
  ASSERT_EQ(stat(path, &buf), -1);
  ASSERT_EQ(errno, ENOENT);
  snprintf(path, PATH_MAX, "/.//..//%s", g_temp_dir_name);
  ASSERT_EQ(stat(path, &buf), -1);
  ASSERT_EQ(errno, ENOENT);
  snprintf(path, PATH_MAX, "../%s", g_temp_dir_name);
  ASSERT_EQ(stat(path, &buf), -1);
  ASSERT_EQ(errno, ENOENT);
  snprintf(path, PATH_MAX, "%s/../..", g_temp_sub_dir_name);
  ASSERT_EQ(stat(path, &buf), 0);
  ASSERT_EQ(root_inode, buf.st_ino);

  passed("test_information_leak", "all");
}

void test_parent_directory_access() {
  // We should be able to access valid paths using "..", as long as they are
  // within our root directory.
  char path[PATH_MAX];
  struct stat buf;
  ASSERT_EQ_MSG(chdir("/"), 0, "chdir() failed");

  ASSERT_EQ(stat("/", &buf), 0);
  const ino_t root_inode = buf.st_ino;
  snprintf(path, PATH_MAX, "%s", g_temp_sub_dir_path);
  ASSERT_EQ(stat(path, &buf), 0);
  const ino_t subdir_inode = buf.st_ino;
  ASSERT_NE(root_inode, subdir_inode);

  // Test valid absolute accesses with ".."
  snprintf(path, PATH_MAX, "%s/..", g_temp_sub_dir_path);
  ASSERT_EQ(stat(path, &buf), 0);
  ASSERT_EQ(root_inode, buf.st_ino);
  snprintf(path, PATH_MAX, "%s/../.", g_temp_sub_dir_path);
  ASSERT_EQ(stat(path, &buf), 0);
  ASSERT_EQ(root_inode, buf.st_ino);

  // Test valid relative accesses with ".."
  snprintf(path, PATH_MAX, "%s/../.", g_temp_sub_dir_name);
  ASSERT_EQ(stat(path, &buf), 0);
  ASSERT_EQ(root_inode, buf.st_ino);
  snprintf(path, PATH_MAX, "./%s/../.", g_temp_sub_dir_name);
  ASSERT_EQ(stat(path, &buf), 0);
  ASSERT_EQ(root_inode, buf.st_ino);

  // Test invalid relative accesses with "..".
  snprintf(path, PATH_MAX, "%s/file_does_not_exist/..", g_temp_sub_dir_name);
  ASSERT_EQ(stat(path, &buf), -1);
  ASSERT_EQ(errno, ENOENT);

  passed("test_parent_directory_access", "all");
}

void test_valid_file_access() {
  // Show that reads and writes to valid files work.
  char file_name[PATH_MAX];
  ASSERT_EQ_MSG(chdir("/"), 0, "chdir() failed");

  // Absolute path
  snprintf(file_name, PATH_MAX, "%s", g_temp_file_path);
  do_test_write_read_file(file_name, /* new_file= */ false);

  // Relative path
  snprintf(file_name, PATH_MAX, "%s", g_temp_file_name);
  do_test_write_read_file(file_name, /* new_file= */ false);

  // Absolute path
  snprintf(file_name, PATH_MAX, "%s/%s", g_temp_sub_dir_path,
           g_temp_sub_file_name);
  do_test_write_read_file(file_name, /* new_file= */ false);

  // Relative path
  snprintf(file_name, PATH_MAX, "%s/%s", g_temp_sub_dir_name,
           g_temp_sub_file_name);
  do_test_write_read_file(file_name, /* new_file= */ false);

  ASSERT_EQ_MSG(chdir(g_temp_sub_dir_name), 0, "chdir() failed");

  // Relative path
  snprintf(file_name, PATH_MAX, "%s", g_temp_sub_file_name);
  do_test_write_read_file(file_name, /* new_file= */ false);

  passed("test_valid_file_access", "all");
}

void test_new_file_access() {
  // Create a new file, show that it is readable / writable.
  char file_name[PATH_MAX];
  do_test_write_read_file("/new_temp_file", true);

  snprintf(file_name, PATH_MAX, "%s/newer_temp_file", g_temp_sub_dir_path);
  do_test_write_read_file(file_name, true);

  snprintf(file_name, PATH_MAX, "%s/..newer_temp_file", g_temp_sub_dir_path);
  do_test_write_read_file(file_name, true);

  passed("test_new_file_access", "all");
}

/*
 * function testSuite()
 *
 *   Run through a complete sequence of file tests.
 */

void testSuite() {
  test_directory_walk();
  test_new_directory_access();
  test_link_access();
  test_symlink_access();
  test_rename_access();
  test_escape_attempt();
  test_information_leak();
  test_parent_directory_access();
  test_valid_file_access();
  test_new_file_access();
}

}  // anonymous namespace

/*
 * main entry point.
 *
 * run all tests and call system exit with appropriate value.
 */
int main(const int argc, const char *argv[]) {
  if (argc != 8) {
    printf("Unexpected arguments\n");
    exit(-1);
  }

  snprintf(g_temp_dir_name, PATH_MAX, "%s", argv[1]);
  snprintf(g_temp_dir_path, PATH_MAX, "/%s", argv[1]);

  snprintf(g_temp_file_name, PATH_MAX, "%s", argv[2]);
  snprintf(g_temp_file_path, PATH_MAX, "/%s", argv[2]);

  snprintf(g_temp_symlink_name, PATH_MAX, "%s", argv[3]);
  snprintf(g_temp_symlink_path, PATH_MAX, "/%s", argv[3]);

  snprintf(g_temp_sub_dir_name, PATH_MAX, "%s", argv[4]);
  snprintf(g_temp_sub_dir_path, PATH_MAX, "/%s", argv[4]);

  snprintf(g_temp_sub_file_name, PATH_MAX, "%s", argv[5]);
  snprintf(g_temp_sub_file_path, PATH_MAX, "/%s/%s",
           g_temp_sub_dir_name, argv[5]);

  snprintf(g_temp_inaccessible_dir_name, PATH_MAX, "%s", argv[6]);
  snprintf(g_temp_inaccessible_file_name, PATH_MAX, "%s", argv[7]);

  // Run the full test suite.
  testSuite();
  printf("All tests PASSED\n");
  exit(0);
}
